/*
  Copyright (C) 2004-2005 SKYRIX Software AG
  
  This file is part of SOPE.
  
  SOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.
  
  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.
  
  You should have received a copy of the GNU Lesser General Public
  License along with SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include "iCalRecurrenceCalculator.h"

@interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
@end

#include <NGExtensions/NGCalendarDateRange.h>
#include "iCalRecurrenceRule.h"
#include "NSCalendarDate+ICal.h"
#include "common.h"
#include <string.h>

@interface iCalRecurrenceCalculator(PrivateAPI)
- (NSCalendarDate *)lastInstanceStartDate;
@end

// #define HEAVY_DEBUG 1

@implementation iCalMonthlyRecurrenceCalculator

typedef BOOL NGMonthSet[12];
typedef BOOL NGMonthDaySet[32]; // 0 is unused

static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
  register unsigned i;
  
  for (i = 1; i <= 31; i++)
    (*daySet)[i] = NO;
}

static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
                                      BOOL doCopy)
{
  register unsigned i;
  
  if (doCopy)
    memcpy(base, new, sizeof(NGMonthDaySet));
  else {
    for (i = 1; i <= 31; i++) {
      if (!(*new)[i])
        (*base)[i] = NO;
    }
  }
}

static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
                                             NSArray *byMonthDay)
{
  /* list of days in the month */
  unsigned i, count;
  BOOL ok;
  
  NGMonthDaySet_clear(daySet);

  for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
    int dayInMonth; /* -31..-1 and 1..31 */
        
    if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
      ok = NO;
      continue; /* invalid value */
    }
    if (dayInMonth > 31) {
      ok = NO;
      continue; /* error, value to large */
    }
    if (dayInMonth < -31) {
      ok = NO;
      continue; /* error, value to large */
    }
    
    /* adjust negative days */
        
    if (dayInMonth < 0) {
      /* eg: -1 == last day in month, 30 days => 30 */
      dayInMonth = 32 - dayInMonth /* because we count from 1 */;
    }
    
    (*daySet)[dayInMonth] = YES;
  }
  return ok;
}

static inline unsigned iCalDoWForNSDoW(int dow) {
  switch (dow) {
  case 0: return iCalWeekDaySunday;
  case 1: return iCalWeekDayMonday;
  case 2: return iCalWeekDayTuesday;
  case 3: return iCalWeekDayWednesday;
  case 4: return iCalWeekDayThursday;
  case 5: return iCalWeekDayFriday;
  case 6: return iCalWeekDaySaturday;
  case 7: return iCalWeekDaySunday;
  default: return 0;
  }
}

#if HEAVY_DEBUG
static NSString *dowEN[8] = { 
  @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
};
#endif

static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
                                         unsigned dayMask,
					 unsigned firstDoWInMonth,
					 unsigned numberOfDaysInMonth,
                                         int occurrence1)
{
  // TODO: this is called 'X' because the API doesn't allow for full iCalendar
  //       functionality. The daymask must be a list of occurence+dow
  register unsigned dayInMonth;
  register int dow; /* current day of the week */
  int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
  
  NGMonthDaySet_clear(daySet);
  
  if (occurrence1 >= 0) {
    for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
      // TODO: complete me
      
      if (dayMask & iCalDoWForNSDoW(dow)) {
        if (occurrence1 == 0)
	  (*daySet)[dayInMonth] = YES;
        else { /* occurrence1 > 0 */
	  occurrences[dow] = occurrences[dow] + 1;
	  
	  if (occurrences[dow] == occurrence1) 
	    (*daySet)[dayInMonth] = YES;
        }
      }
      
      dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
    }
  }
  else {
    int lastDoWInMonthSet;
    
    /* get the last dow in the set (not necessarily the month!) */
    for (dayInMonth = 1, dow = firstDoWInMonth; 
	 dayInMonth < numberOfDaysInMonth;dayInMonth++)
      dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
    lastDoWInMonthSet = dow;
    
#if HEAVY_DEBUG
    NSLog(@"LAST DOW IN SET: %i / %@", 
	  lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
#endif
    /* start at the end of the set */
    for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet; 
	 dayInMonth >= 1; dayInMonth--) {
      // TODO: complete me
      
#if HEAVY_DEBUG
      NSLog(@"  CHECK day-of-month %02i, "
	    @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
	    dayInMonth, 
	    dow, dowEN[dow],
	    firstDoWInMonth, dowEN[firstDoWInMonth],
	    lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
	    );
#endif
      
      if (dayMask & iCalDoWForNSDoW(dow)) {
	occurrences[dow] = occurrences[dow] + 1;
#if HEAVY_DEBUG
	NSLog(@"    MATCH %i/%@ count: %i occurences=%i",
	      dow, dowEN[dow], occurrences[dow], occurrence1);
#endif
	  
	if (occurrences[dow] == -occurrence1) {
#if HEAVY_DEBUG
	  NSLog(@"    COUNT MATCH");
#endif
	  (*daySet)[dayInMonth] = YES;
	}
      }
      
      dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
    }
  }
}

- (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
  limitDate:(NSCalendarDate *)_until
  limitRange:(NGCalendarDateRange *)_r
  toArray:(NSMutableArray *)_ranges
{
  NGCalendarDateRange *r;
  NSCalendarDate *end;
  
  /* check whether we are still in the limits */

  // TODO: I think we should check in here whether we succeeded the
  //       repeatCount. Currently we precalculate that info in the
  //       -lastInstanceStartDate method.
  if (_until != nil) {
    /* Note: the 'until' in the rrule is inclusive as per spec */
    if ([_until compare:_startDate] == NSOrderedAscending)
      /* start after until */
      return NO; /* Note: we assume that the algorithm is sequential */
  }

  /* create end date */

  end = [_startDate addTimeInterval:[self->firstRange duration]];
  [end setTimeZone:[_startDate timeZone]];
    
  /* create range and check whether its in the requested range */
  
  r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
  if ([_r containsDateRange:r])
    [_ranges addObject:r];
  [r release]; r = nil;
  
  return YES;
}

- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
  /* main entry */
  // TODO: check whether this is OK for multiday-events!
  NSMutableArray *ranges;
  NSTimeZone     *timeZone;
  NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
  int            eventDayOfMonth;
  unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
  int            diff;
  NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
    YES, YES, YES, YES, YES, YES, 
    YES, YES, YES, YES, YES, YES
  };
  NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
  NGMonthDaySet byMonthDaySet;
  
  eventStartDate  = [self->firstRange startDate];
  eventDayOfMonth = [eventStartDate dayOfMonth];
  timeZone   = [eventStartDate timeZone];
  rStart     = [_r startDate];
  rEnd       = [_r endDate];
  interval   = [self->rrule repeatInterval];
  until      = [self lastInstanceStartDate]; // TODO: maybe replace
  byMonthDay = [self->rrule byMonthDay];
  

  /* check whether the range to be processed is beyond the 'until' date */
  
  if (until != nil) {
    if ([until compare:rStart] == NSOrderedAscending) /* until before start */
      return nil;
    if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
      rEnd = until; // TODO: why is that? end is _before_ until?
  }

  
  /* precalculate month days (same for all instances) */

  if (byMonthDay != nil)
    NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
  
  
  // TODO: I think the 'diff' is to skip recurrence which are before the
  //       requested range. Not sure whether this is actually possible, eg
  //       the repeatCount must be processed from the start.
  diff = [eventStartDate monthsBetweenDate:rStart];
  if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
    diff = -diff;
  
  numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
  ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
  
  /* 
     Note: we do not add 'eventStartDate', this is intentional, the event date
           itself is _not_ necessarily part of the sequence, eg with monthly
           byday recurrences.
  */
  
  for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
       monthIdxInRange++) {
    NSCalendarDate *cursor;
    unsigned       numDaysInMonth;
    int            monthIdxInRecurrence, dom;
    NGMonthDaySet  monthDays;
    BOOL           didByFill, doCont;
    
    monthIdxInRecurrence = diff + monthIdxInRange;
    
    if (monthIdxInRecurrence < 0)
      continue;
    
    /* first check whether we are in the interval */
    
    if ((monthIdxInRecurrence % interval) != 0)
      continue;

    /*
      Then the sequence is:
      - check whether the month is in the BYMONTH list
    */
    
    cursor = [eventStartDate dateByAddingYears:0
                             months:(diff + monthIdxInRange)
                             days:0];
    [cursor setTimeZone:timeZone];
    numDaysInMonth = [cursor numberOfDaysInMonth];
    

    /* check whether we match the bymonth specification */
    
    if (!byMonthList[[cursor monthOfYear] - 1])
      continue;
    
    
    /* check 'day level' byXYZ rules */
    
    didByFill = NO;
    
    if (byMonthDay != nil) { /* list of days in the month */
      NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
      didByFill = YES;
    }
    
    if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
      NGMonthDaySet ruleset;
      unsigned firstDoWInMonth;
      
      firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
      
      NGMonthDaySet_fillWithByDayX(&ruleset, 
                                   [self->rrule byDayMask],
				   firstDoWInMonth,
				   [cursor numberOfDaysInMonth],
                                   [self->rrule byDayOccurence1]);
      NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
      didByFill = YES;
    }
    
    if (!didByFill) {
      /* no rules applied, take the dayOfMonth of the startDate */
      NGMonthDaySet_clear(&monthDays);
      monthDays[eventDayOfMonth] = YES;
    }
    
    // TODO: add processing of byhour/byminute/bysecond etc
    
    for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
      NSCalendarDate *start;
      
      if (!monthDays[dom])
	continue;
      
      if (eventDayOfMonth == dom)
	start = cursor;
      else {
	start = [cursor dateByAddingYears:0 months:0
			days:(dom - eventDayOfMonth)];
      }
      
      doCont = [self _addInstanceWithStartDate:start
		     limitDate:until
		     limitRange:_r
		     toArray:ranges];
    }
    if (!doCont) break; /* reached some limit */
  }
  return ranges;
}

- (NSCalendarDate *)lastInstanceStartDate {
  if ([self->rrule repeatCount] > 0) {
    NSCalendarDate *until;
    unsigned       months, interval;
    
    interval = [self->rrule repeatInterval];
    months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
    
    if (interval > 0)
      months *= interval;
    
    until = [[self->firstRange startDate] dateByAddingYears:0
                                          months:months
                                          days:0];
    return until;
  }
  return [super lastInstanceStartDate];
}

@end /* iCalMonthlyRecurrenceCalculator */
